home *** CD-ROM | disk | FTP | other *** search
/ Delphi Magazine Collection 2001 / Delphi Magazine Collection 20001 (2001).iso / DISKS / Issue40 / Diagram / JimParse.pas < prev    next >
Encoding:
Pascal/Delphi Source File  |  1998-08-17  |  11.8 KB  |  463 lines

  1. unit JimParse;
  2.  
  3. interface
  4.  
  5. uses
  6.   SysUtils, Classes;
  7.  
  8.  
  9. type
  10.   TjimToken = class(TObject)
  11.   private
  12.     FTokenType : Integer;
  13.     FAsString  : string;
  14.   public
  15.     property TokenType : Integer read FTokenType write FTokenType;
  16.     property AsString  : string read FAsString write FAsString;
  17.   end;
  18.  
  19.  
  20.   TjimLexicalAnalyser = class(TObject)
  21.   private
  22.     FText     : string;
  23.     FPosition : Integer;
  24.  
  25.     procedure SetText(const Value : string);
  26.   public
  27.     constructor Create;
  28.  
  29.     procedure GetNextToken(NextToken : TjimToken);
  30.     property  Text : string read FText write SetText;
  31.   end;
  32.  
  33.  
  34.   TjimSymbolType = (stTitle,stBase,stLink,stImage);
  35.  
  36.  
  37.   TjimSymbol = class(TCollectionItem)
  38.   private
  39.     FSymbolType  : TjimSymbolType;
  40.     FSymbolValue : string;
  41.   public
  42.     procedure Assign(Source : TPersistent); override;
  43.  
  44.     property SymbolType  : TjimSymbolType read FSymbolType write FSymbolType;
  45.     property SymbolValue : string read FSymbolValue write FSymbolValue;
  46.   end;
  47.  
  48.  
  49.   TjimSymbolTable = class(TCollection)
  50.   private
  51.     function  GetItem(Index : Integer) : TjimSymbol;
  52.     procedure SetItem(Index : Integer;Value : TjimSymbol);
  53.   public
  54.     function Add : TjimSymbol;
  55.     function AddSymbol(SymType : TjimSymbolType;SymValue : string) : TjimSymbol;
  56.  
  57.     property Items[Index : Integer] : TjimSymbol read GetItem write SetItem; default;
  58.   end;
  59.  
  60.  
  61.   TjimHtmlParser = class(TObject)
  62.   private
  63.     FLookahead   : TjimToken;
  64.     FLexAnalyser : TjimLexicalAnalyser;
  65.     FSymbolTable : TjimSymbolTable;
  66.     FLastTag     : string;
  67.  
  68.     procedure Match(T : Integer);
  69.     procedure ConsumeWhiteSpace;
  70.     procedure Document;
  71.     procedure Tag;
  72.     procedure Data;
  73.     procedure TagName;
  74.     procedure AttributeList;
  75.     function  AttributeName : string;
  76.     function  Value : string;
  77.     function  Identifier : string;
  78.     function  QuotedValue : string;
  79.     function  PlainValue : string;
  80.   public
  81.     constructor Create;
  82.     destructor  Destroy; override;
  83.  
  84.     procedure Parse(const DocString : string);
  85.  
  86.     property SymbolTable : TjimSymbolTable read FSymbolTable;
  87.   end;
  88.  
  89.  
  90.   EjimHtmlParserError = class(Exception);
  91.  
  92.  
  93. implementation
  94.  
  95. const
  96.   // Token types are the characters 0 to 255, along with the following
  97.   ttEndOfDoc = -1;
  98.  
  99.  
  100.  
  101. // --------------------------- TjimLexicalAnalyser ---------------------------
  102.  
  103. constructor TjimLexicalAnalyser.Create;
  104. begin {Create}
  105.   FText     := '';
  106.   FPosition := 0;
  107. end;  {Create}
  108.  
  109.  
  110. procedure TjimLexicalAnalyser.SetText(const Value : string);
  111. begin {SetText}
  112.   if FText = Value then begin
  113.     // Only proceed if setting a new string
  114.     Exit;
  115.   end;
  116.  
  117.   FPosition := 1;
  118.   FText     := Value;
  119. end;  {SetText}
  120.  
  121.  
  122. procedure TjimLexicalAnalyser.GetNextToken(NextToken : TjimToken);
  123. begin {GetNextToken}
  124.  
  125.   // Read the next character
  126.   if FPosition > Length(FText) then begin
  127.     // At the end of the document
  128.     NextToken.AsString  := #0;
  129.     NextToken.TokenType := ttEndOfDoc;
  130.   end else begin
  131.     // Return the character
  132.     NextToken.AsString  := FText[FPosition];
  133.     NextToken.TokenType := Integer(FText[FPosition]);
  134.     Inc(FPosition);
  135.   end;
  136. end;  {GetNextToken}
  137.  
  138.  
  139. // ------------------------------- TjimSymbol -------------------------------
  140.  
  141. procedure TjimSymbol.Assign(Source : TPersistent);
  142. begin {Assign}
  143.   if Source is TjimSymbol then begin
  144.     SymbolType  := TjimSymbol(Source).SymbolType;
  145.     SymbolValue := TjimSymbol(Source).SymbolValue;
  146.     Exit;
  147.   end;
  148.  
  149.   inherited Assign(Source);
  150. end;  {Assign}
  151.  
  152.  
  153. // ------------------------------ TjimSymbolTable ----------------------------
  154.  
  155. function TjimSymbolTable.GetItem(Index : Integer) : TjimSymbol;
  156. begin {GetItem}
  157.   Result := TjimSymbol(inherited GetItem(Index));
  158. end;  {GetItem}
  159.  
  160.  
  161. procedure TjimSymbolTable.SetItem(Index : Integer;Value : TjimSymbol);
  162. begin {SetItem}
  163.   inherited SetItem(Index,Value);
  164. end;  {SetItem}
  165.  
  166.  
  167. function TjimSymbolTable.Add : TjimSymbol;
  168. begin {Add}
  169.   Result := TjimSymbol(inherited Add);
  170. end;  {Add}
  171.  
  172.  
  173. function TjimSymbolTable.AddSymbol(SymType : TjimSymbolType;SymValue : string) : TjimSymbol;
  174.   var
  175.     i : Integer;
  176. begin {AddSymbol}
  177.   Result := nil;
  178.   
  179.   // Check whether symbol is already in the list
  180.   for i := 0 to Count - 1 do begin
  181.     if (Items[i].SymbolType = SymType) and (Items[i].SymbolValue = SymValue) then begin
  182.       Exit;
  183.     end;
  184.   end;
  185.  
  186.   Result := Add;
  187.   Result.SymbolType  := SymType;
  188.   result.SymbolValue := SymValue;
  189. end;  {AddSymbol}
  190.  
  191.  
  192. // ------------------------------ TjimHtmlParser -----------------------------
  193.  
  194. procedure TjimHtmlParser.ConsumeWhiteSpace;
  195.   // Eats 'whitespace' ie chars 0 to 32 inclusive. Here instead of lexical
  196.   // analyser because white space is allowed sometimes.
  197. begin {ConsumeWhiteSpace}
  198.   while (FLookahead.TokenType <> ttEndOfDoc) and
  199.         (FLookAhead.AsString <= ' ') do begin
  200.     FLexAnalyser.GetNextToken(FLookAhead);
  201.   end;
  202. end;  {ConsumeWhiteSpace}
  203.  
  204.  
  205. procedure TjimHtmlParser.Match(T : Integer);
  206.   // If the token type T matches the FLookahead token type then FLookAhead is
  207.   // set to the next token, otherwise an exception is raised
  208. begin {Match}
  209.   if FLookahead.TokenType = T then begin
  210.     FLexAnalyser.GetNextToken(FLookahead);
  211.   end else begin
  212.     raise EjimHtmlParserError.Create('HTML syntax error. Expected ' +
  213.                                      IntToStr(FLookahead.TokenType));
  214.   end;
  215. end;  {Match}
  216.  
  217.  
  218. procedure TjimHtmlParser.Document;
  219. begin {Document}
  220.   while FLookahead.TokenType <> ttEndOfDoc do begin
  221.     ConsumeWhiteSpace;
  222.  
  223.     if FLookahead.AsString = '<' then begin
  224.       Tag;
  225.     end else begin
  226.       Data;
  227.     end;
  228.   end;
  229.  
  230.   Match(ttEndOfDoc);
  231. end;  {Document}
  232.  
  233.  
  234. procedure TjimHtmlParser.Tag;
  235. begin {Tag}
  236.   Match(Ord('<'));
  237.   ConsumeWhiteSpace;
  238.  
  239.   if FLookahead.AsString = '/' then begin
  240.     // Finding an end tag
  241.     Match(Ord('/'));
  242.     FLastTag := '/';
  243.     ConsumeWhiteSpace;
  244.     TagName;
  245.   end else begin
  246.     // Finding a start tag, or a tag that doesn't enclose anything
  247.     FLastTag := '';
  248.     ConsumeWhiteSpace;
  249.     TagName;
  250.     ConsumeWhiteSpace;
  251.     AttributeList;
  252.   end;
  253.  
  254.   Match(Ord('>'));
  255. end;  {Tag}
  256.  
  257.  
  258. procedure TjimHtmlParser.Data;
  259.   var
  260.     TitleStr : string;
  261. begin {Data}
  262.   TitleStr := '';
  263.  
  264.   while (FLookahead.AsString <> '<') and
  265.         (FLookahead.TokenType <> ttEndOfDoc) do begin
  266.     // Collect the title string. It is ok to search like this because no other
  267.     // tags are allowed in a title
  268.     if CompareText(FLastTag,'Title') = 0 then begin
  269.       TitleStr := TitleStr + FLookahead.AsString;
  270.     end;
  271.  
  272.     Match(FLookahead.TokenType);
  273.   end;
  274.  
  275.   if TitleStr > '' then begin
  276.     FSymbolTable.AddSymbol(stTitle,TitleStr);
  277.   end;
  278. end;  {Data}
  279.  
  280.  
  281. procedure TjimHtmlParser.TagName;
  282. begin {TagName}
  283.   FLastTag := FLastTag + Identifier;
  284.  
  285.   if FLastTag = '!--' then begin
  286.     // In a comment tag. Treat this specially by ignoring all characters
  287.     // until the end of the comment tag
  288.     repeat
  289.       if FLookahead.AsString = '-' then begin
  290.         FLastTag := FLastTag + FLookahead.AsString;
  291.       end else begin
  292.         FLastTag := '';
  293.       end;
  294.  
  295.       Match(FLookahead.TokenType);
  296.     until FLastTag = '--';
  297.   end else if CompareText(FLastTag,'META') = 0 then begin
  298.     // In a META tag. There is all sorts of rubbish here, so consume it all
  299.     // until the end of the tag
  300.     while FLookahead.AsString <> '>' do begin
  301.       Match(FLookahead.TokenType);
  302.     end;
  303.   end;
  304. end;  {TagName}
  305.  
  306.  
  307. procedure TjimHtmlParser.AttributeList;
  308.   var
  309.     FLastAttribute : string;
  310.     FLastValue     : string;
  311. begin {AttributeList}
  312.   while FLookahead.AsString <> '>' do begin
  313.     FLastAttribute := AttributeName;
  314.     ConsumeWhiteSpace;
  315.  
  316.     if FLookahead.AsString = '=' then begin
  317.       Match(Ord('='));
  318.       ConsumeWhiteSpace;
  319.       FLastValue := Value;
  320.       ConsumeWhiteSpace;
  321.  
  322.       // Should only get here if FLastAttribute is not an empty string
  323.       if (CompareText(FLastTag,'BASE') = 0) and
  324.          (CompareText('HREF',FLastAttribute) = 0) then begin
  325.         // Special case when found the HREF attribute of a BASE tag
  326.         FSymbolTable.AddSymbol(stBase,FLastValue);
  327.       end else if (CompareText(FLastTag,'IMG') = 0) and
  328.          (CompareText('SRC',FLastAttribute) = 0) then begin
  329.         // Found an image
  330.         FSymbolTable.AddSymbol(stImage,FLastValue);
  331.       end else if ((CompareText(FLastTag,'A') = 0) or
  332.                    (CompareText(FLastTag,'AREA') = 0) or
  333.                    (CompareText(FLastTag,'LINK') = 0)) and
  334.                   (CompareText('HREF',FLastAttribute) = 0) then begin
  335.         // Found an ordinary link
  336.         FSymbolTable.AddSymbol(stLink,FLastValue);
  337.       end else if (CompareText(FLastTag,'FRAME') = 0) and
  338.                   (CompareText('SRC',FLastAttribute) = 0) then begin
  339.         // Found an ordinary link
  340.         FSymbolTable.AddSymbol(stLink,FLastValue);
  341.       end;
  342.     end;
  343.   end;
  344. end;  {AttributeList}
  345.  
  346.  
  347. function TjimHtmlParser.AttributeName : string;
  348. begin {AttributeName}
  349.   Result := '';
  350.  
  351.   if FLookahead.AsString = '"' then begin
  352.     Result := QuotedValue;
  353.   end else begin
  354.     Result := Identifier;
  355.   end;
  356. end;  {AttributeName}
  357.  
  358.  
  359. function TjimHtmlParser.Value : string;
  360. begin {Value}
  361.   Result := '';
  362.  
  363.   if FLookahead.AsString = '"' then begin
  364.     Result := QuotedValue;
  365.   end else begin
  366.     Result := PlainValue;
  367.   end;
  368. end;  {Value}
  369.  
  370.  
  371. function TjimHtmlParser.Identifier : string;
  372.   const
  373.       IdentifierSet = ['A'..'Z','a'..'z','0'..'9','-','!'];
  374. begin {Identifier}
  375.   Result := '';
  376.  
  377.   if (Length(FLookahead.AsString) >= 1) and
  378.      (not (FLookahead.AsString[1] in IdentifierSet)) then begin
  379.     raise EjimHtmlParserError.Create('HTML syntax error. Expected identifier, ' +
  380.                                      'but got : ' + FLookahead.AsString +
  381.                                      ' in tag ' + FLastTag);
  382.   end;
  383.  
  384.   repeat
  385.     Result := Result + FLookahead.AsString;
  386.  
  387.     if Result = '!--' then begin
  388.       // Found a comment tag. Some people eg Microsoft, don't put a space after
  389.       // this part of the tag
  390.       Exit;
  391.     end;
  392.  
  393.     Match(FLookahead.TokenType);
  394.   until not (FLookahead.AsString[1] in IdentifierSet);
  395. end;  {Identifier}
  396.  
  397.  
  398. function TjimHtmlParser.QuotedValue : string;
  399. begin {QuotedValue}
  400.   Result := '';
  401.   Match(Ord('"'));
  402.  
  403.   while FLookahead.AsString <> '"' do begin
  404.     Result := Result + FLookahead.AsString;
  405.     Match(FLookahead.TokenType);
  406.   end;
  407.  
  408.   Match(Ord('"'));
  409. end;  {QuotedValue}
  410.  
  411.  
  412. function TjimHtmlParser.PlainValue : string;
  413.   const
  414.       PlainValueSet = ['A'..'Z','a'..'z','0'..'9','-','.','+','-',':','/','?',
  415.                        ''''];
  416. begin {PlainValue}
  417.   Result := '';
  418.  
  419.   if (Length(FLookahead.AsString) >= 1) and
  420.      (not (FLookahead.AsString[1] in PlainValueSet)) then begin
  421.     raise EjimHtmlParserError.Create('HTML syntax error. Expected plain value, ' +
  422.                                      'but got : ' + FLookahead.AsString +
  423.                                      ' in tag ' + FLastTag);
  424.   end;
  425.  
  426.   repeat
  427.     Result := Result + FLookahead.AsString;
  428.     Match(FLookahead.TokenType);
  429.   until not (FLookahead.AsString[1] in PlainValueSet);
  430. end;  {PlainValue}
  431.  
  432.  
  433. constructor TjimHtmlParser.Create;
  434. begin {Create}
  435.   FLookahead   := TjimToken.Create;
  436.   FLexAnalyser := TjimLexicalAnalyser.Create;
  437.   FSymbolTable := TjimSymbolTable.Create(TjimSymbol);
  438. end;  {Create}
  439.  
  440.  
  441. destructor TjimHtmlParser.Destroy;
  442. begin {Destroy}
  443.   FLookahead.Free;
  444.   FLexAnalyser.Free;
  445.   FSymbolTable.Free;
  446. end;  {Destroy}
  447.  
  448.  
  449. procedure TjimHtmlParser.Parse(const DocString : string);
  450. begin {Parse}
  451.   if DocString = '' then begin
  452.     Exit;
  453.   end;
  454.  
  455.   FLastTag          := '';
  456.   FLexAnalyser.Text := DocString;
  457.   FLexAnalyser.GetNextToken(FLookahead);
  458.   Document;
  459. end;  {Parse}
  460.  
  461.  
  462. end.
  463.